Skip to content

balance fetcher to fix non-eth assetIds#8284

Merged
bergarces merged 19 commits intomainfrom
fix-rpc-balance-fetcher
Mar 25, 2026
Merged

balance fetcher to fix non-eth assetIds#8284
bergarces merged 19 commits intomainfrom
fix-rpc-balance-fetcher

Conversation

@bergarces
Copy link
Copy Markdown
Contributor

@bergarces bergarces commented Mar 24, 2026

Explanation

Fixes issue in which the balance appears with the wrong assetId forslip44 evm native assets that are not ETH.

BalanceFetcher now requires the list of tokens to include assetId, address and the native token.

Before (basic functionality off, Polygon using RPC and the wrong assetId)
image

With basic functionality on, the Polygon balance uses the correct assetId (as it comes from the api), but Avalanche is still wrong.

After (basic functionality off):
image

Tested also with basic functionality on, which then has prices.

References

Checklist

  • I've updated the test suite for new or updated code as appropriate
  • I've updated documentation (JSDoc, Markdown, etc.) for new or updated code as appropriate
  • I've communicated my changes to consumers by updating changelogs for packages I've changed
  • I've introduced breaking changes in this PR and have prepared draft pull requests for clients and consumer packages to resolve them

Note

Medium Risk
Refactors the EVM RPC balance fetching pipeline and changes BalanceFetcher’s public API, which could impact balance polling and custom asset balance retrieval across chains if any call sites weren’t updated correctly.

Overview
Fixes incorrect assetId reporting for EVM native assets whose CAIP-19 identifier is not eip155:<chain>/slip44:60 (e.g. Avalanche), by making callers pass the native asset ID through the RPC balance pipeline instead of having BalanceFetcher reconstruct it.

RpcDataSource now builds a unified AssetFetchEntry[] (native + custom ERC-20s, with optional known decimals) and calls BalanceFetcher.fetchBalancesForAssets, which maps multicall results back to the original assetId. The old token-address based API (fetchBalancesForTokens, TokenFetchInfo, BalanceFetchOptions) is removed, tests are updated accordingly, and a shared ZERO_ADDRESS constant is introduced.

Written by Cursor Bugbot for commit 2de1d56. This will update automatically on new commits. Configure here.

@bergarces bergarces requested review from a team as code owners March 24, 2026 14:23
Copy link
Copy Markdown
Contributor Author

@bergarces bergarces Mar 24, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The goal here was to pass the assetId alongside the token addresses and the tokenInfo to the balance fetcher.

Since the last two were already being placed in two different arrays of the same length, as well as a setting to return native balance, it's all been simplified by just passing to balance fetcher a single array that contains everything needed:

  • AssetId (including the native token)
  • Address
  • TokenInfo (this is just the decimals)

That way, BalanceFetcher does not need to know how to build the native assetId for every chain.

Also, the token address passed to balance fetcher is always the zero address, regardless of the chain, as that is how BalanceFetcher determines whether to fetch the balance of a token or the balance of the account.

* native assets even when the chain has a native asset with a non-zero address)
* and optional metadata
*/
export type BalanceFetchOptions = {
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This wasn't being used at all, only to specify that we wanted to include native assets, which was already the default. In any case the native assets need to be added to the array of tokens to fetch now.

/** Token decimals (omit when unknown — balance fetcher returns raw balance for RpcDataSource to resolve). */
decimals?: number;
/** Token symbol (optional) */
symbol?: string;
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This wasn't being used at all.

continue;
}

const isNative = assetNamespace === 'slip44';
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is not completely right, but this is how we are currently determining that an asset is native everywhere else, so this is consistent.

We already have a ticket and plans to discuss how to handle native tokens (by pushing the definition to an API).

const tokenMap = new Map<string, TokenFetchInfo>();

for (const assetId of Object.keys(accountBalances)) {
// Only process ERC20 tokens on the current chain
Copy link
Copy Markdown
Contributor Author

@bergarces bergarces Mar 24, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We no longer need to filter only erc20 tokens, since we need to include all assets for that chain, including native assets.

This simplifies things as well.

Copy link
Copy Markdown

@cursor cursor bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes and found 1 potential issue.

Fix All in Cursor

Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, have a team admin enable autofix in the Cursor dashboard.

@bergarces
Copy link
Copy Markdown
Contributor Author

@metamaskbot publish-preview

@github-actions
Copy link
Copy Markdown
Contributor

Preview builds have been published. Learn how to use preview builds in other projects.

Expand for full list of packages and versions.
@metamask-previews/account-tree-controller@5.0.1-preview-1189e6b40
@metamask-previews/accounts-controller@37.0.0-preview-1189e6b40
@metamask-previews/address-book-controller@7.1.0-preview-1189e6b40
@metamask-previews/ai-controllers@0.5.0-preview-1189e6b40
@metamask-previews/analytics-controller@1.0.0-preview-1189e6b40
@metamask-previews/analytics-data-regulation-controller@0.0.0-preview-1189e6b40
@metamask-previews/announcement-controller@8.0.0-preview-1189e6b40
@metamask-previews/app-metadata-controller@2.0.0-preview-1189e6b40
@metamask-previews/approval-controller@9.0.0-preview-1189e6b40
@metamask-previews/assets-controller@3.1.0-preview-1189e6b40
@metamask-previews/assets-controllers@101.0.1-preview-1189e6b40
@metamask-previews/base-controller@9.0.0-preview-1189e6b40
@metamask-previews/base-data-service@0.0.0-preview-1189e6b40
@metamask-previews/bridge-controller@69.2.1-preview-1189e6b40
@metamask-previews/bridge-status-controller@70.0.1-preview-1189e6b40
@metamask-previews/build-utils@3.0.4-preview-1189e6b40
@metamask-previews/chain-agnostic-permission@1.4.0-preview-1189e6b40
@metamask-previews/claims-controller@0.4.3-preview-1189e6b40
@metamask-previews/client-controller@1.0.0-preview-1189e6b40
@metamask-previews/compliance-controller@1.0.1-preview-1189e6b40
@metamask-previews/composable-controller@12.0.0-preview-1189e6b40
@metamask-previews/config-registry-controller@0.1.1-preview-1189e6b40
@metamask-previews/connectivity-controller@0.1.0-preview-1189e6b40
@metamask-previews/controller-utils@11.19.0-preview-1189e6b40
@metamask-previews/core-backend@6.2.0-preview-1189e6b40
@metamask-previews/delegation-controller@2.0.2-preview-1189e6b40
@metamask-previews/earn-controller@11.1.2-preview-1189e6b40
@metamask-previews/eip-5792-middleware@3.0.1-preview-1189e6b40
@metamask-previews/eip-7702-internal-rpc-middleware@0.1.0-preview-1189e6b40
@metamask-previews/eip1193-permission-middleware@1.0.3-preview-1189e6b40
@metamask-previews/ens-controller@19.1.0-preview-1189e6b40
@metamask-previews/error-reporting-service@3.0.1-preview-1189e6b40
@metamask-previews/eth-block-tracker@15.0.1-preview-1189e6b40
@metamask-previews/eth-json-rpc-middleware@23.1.0-preview-1189e6b40
@metamask-previews/eth-json-rpc-provider@6.0.0-preview-1189e6b40
@metamask-previews/foundryup@1.0.1-preview-1189e6b40
@metamask-previews/gas-fee-controller@26.1.0-preview-1189e6b40
@metamask-previews/gator-permissions-controller@2.1.1-preview-1189e6b40
@metamask-previews/geolocation-controller@0.1.1-preview-1189e6b40
@metamask-previews/json-rpc-engine@10.2.3-preview-1189e6b40
@metamask-previews/json-rpc-middleware-stream@8.0.8-preview-1189e6b40
@metamask-previews/keyring-controller@25.1.0-preview-1189e6b40
@metamask-previews/logging-controller@8.0.0-preview-1189e6b40
@metamask-previews/message-manager@14.1.0-preview-1189e6b40
@metamask-previews/messenger@0.3.0-preview-1189e6b40
@metamask-previews/multichain-account-service@7.1.0-preview-1189e6b40
@metamask-previews/multichain-api-middleware@1.2.7-preview-1189e6b40
@metamask-previews/multichain-network-controller@3.0.5-preview-1189e6b40
@metamask-previews/multichain-transactions-controller@7.0.2-preview-1189e6b40
@metamask-previews/name-controller@9.1.0-preview-1189e6b40
@metamask-previews/network-controller@30.0.0-preview-1189e6b40
@metamask-previews/network-enablement-controller@5.0.0-preview-1189e6b40
@metamask-previews/notification-services-controller@23.0.0-preview-1189e6b40
@metamask-previews/permission-controller@12.2.1-preview-1189e6b40
@metamask-previews/permission-log-controller@5.0.0-preview-1189e6b40
@metamask-previews/perps-controller@1.3.0-preview-1189e6b40
@metamask-previews/phishing-controller@17.0.0-preview-1189e6b40
@metamask-previews/polling-controller@16.0.3-preview-1189e6b40
@metamask-previews/preferences-controller@23.0.0-preview-1189e6b40
@metamask-previews/profile-metrics-controller@3.1.1-preview-1189e6b40
@metamask-previews/profile-sync-controller@28.0.0-preview-1189e6b40
@metamask-previews/ramps-controller@12.0.1-preview-1189e6b40
@metamask-previews/rate-limit-controller@7.0.0-preview-1189e6b40
@metamask-previews/react-data-query@0.0.0-preview-1189e6b40
@metamask-previews/remote-feature-flag-controller@4.1.0-preview-1189e6b40
@metamask-previews/sample-controllers@4.0.3-preview-1189e6b40
@metamask-previews/seedless-onboarding-controller@9.0.0-preview-1189e6b40
@metamask-previews/selected-network-controller@26.0.3-preview-1189e6b40
@metamask-previews/shield-controller@5.0.2-preview-1189e6b40
@metamask-previews/signature-controller@39.1.0-preview-1189e6b40
@metamask-previews/storage-service@1.0.0-preview-1189e6b40
@metamask-previews/subscription-controller@6.0.2-preview-1189e6b40
@metamask-previews/transaction-controller@63.1.0-preview-1189e6b40
@metamask-previews/transaction-pay-controller@18.1.0-preview-1189e6b40
@metamask-previews/user-operation-controller@41.1.0-preview-1189e6b40

@bergarces bergarces force-pushed the fix-rpc-balance-fetcher branch from 1189e6b to 4ab304a Compare March 25, 2026 10:15
@bergarces bergarces changed the title balance fetcher changes balance fetcher to fix non-eth assetIds Mar 25, 2026

### Fixed

- Refactored `BalanceFetcher` and `RpcDataSource` to ensure the correct `assetId` is used for EVM native assets that are not ETH ([#8284](https://github.com/MetaMask/core/pull/8284))
Copy link
Copy Markdown
Contributor Author

@bergarces bergarces Mar 25, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

All the method signatures I have changed are internal, none of them are used in the clients. So I'm not marking it as breaking.


assetsToFetch.set(assetIdLowerCase, {
assetId,
address: tokenAddress,
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Here we should also use the state that we fetch in order to get the decimals and pass them down, making it mandatory, not optional.

That stops us from having to guess the decimals afterwards.

I would prefer to do that in a separate PR though.

*/
async _executePoll(input: BalancePollingInput): Promise<void> {
const result = await this.fetchBalances(
const result = await this.#fetchBalances(
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

we're converting this to internal function , this is good i like it

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes. I had a look at the public methods and a couple of them were only being used internally, so I proactively switched them to private for now to make it easier to see what's exposed.

@bergarces bergarces added this pull request to the merge queue Mar 25, 2026
Merged via the queue into main with commit a7df329 Mar 25, 2026
326 checks passed
@bergarces bergarces deleted the fix-rpc-balance-fetcher branch March 25, 2026 12:22
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants